13|多相机

loading

13.1 组合摄像机

在实际游戏项目开发过程中往往需要使用多台相机进行不同方面的渲染,还可能有多个视角观察点,包括常见的分屏功能、小地图、摄像头等等,下面我们来进行多台摄像机的组合应用。

13.1.1 分屏

首先我们进行两个摄像机并排分屏的应用,左侧相机的Viewport Rect宽度W设置为0.5,右边相机的宽度也设置为0.5,另外X设置为0.5,如果没有启用后处理,那么画面分屏显示是正常的。

loading

如果启用了后处理就会失效,两个摄像机都进行了全屏渲染,最终都覆盖了整个相机的目标缓冲区且只有最后一个可见。

loading

​这是因为调用SetRenderTarget方法会重置视口Viewport以覆盖整个目标,若要将视口应用到最终的Post FX Pass中,我们必须在设置渲染目标后和渲染绘制之前设置视口。

复制PostFXStack脚本的Draw方法并命名为DrawFinal,去掉后面两个传参。在设置渲染目标后调用SetViewport方法设置视口,因为这是最终的绘制,所以调用DrawProcedural方法时使用Final pass,最后在DoColorGradingAndToneMapping方法的末尾调用它。

 void DrawFinal(RenderTargetIdentifier from) 
{
buffer.SetGlobalTexture(fxSourceId, from);
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
//设置视口
buffer.SetViewport(camera.pixelRect);
buffer.DrawProcedural(Matrix4x4.identity, settings.Material, (int)Pass.Final, MeshTopology.Triangles, 3);
}

void DoColorGradingAndToneMapping(int sourceId)
{
...
//Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Final);
DrawFinal(sourceId);
buffer.ReleaseTemporaryRT(colorGradingLUTId);
}

现在启用后处理也可以正常显示了。

loading

13.1.2 相机分层

除了渲染到单独的区域,我们还可以让相机视口重叠,我们保持第一个相机的Viewport Rect默认参数,把第二个相机的XY设置为0.25,W和H设置为0.5,缩小一半。

loading

​如果未启用后处理,可以将顶部相机的Clear Flags设置为Depth Only,这将使顶部相机渲染时仅清除深度缓冲,天空盒部分由下面的图层混合覆盖,但如果启用了后处理则不起作用了,我们可以将Clear Flags设置为Solid Color,我们将看到相机的Background颜色。

loading

loading

1. 为了在启用后处理时也可以进行图层透明混合,我们为PostFXStack.shader的Final Pass设置混合模式,而不是使用默认的One Zero。

 Pass 
{
Name "Final"
Blend SrcAlpha OneMinusSrcAlpha
...
}

2. 在DrawFinal方法中设置渲染目标时使RenderBufferLoadAction使用Load模式来加载目标缓冲区。

 void DrawFinal(RenderTargetIdentifier from) 
{
buffer.SetGlobalTexture(fxSourceId, from);
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
...
}

当顶部相机的Background背景色的Alpha为0时,禁用Bloom效果发现能达到理想效果,但如果启用了Bloom会发现还是和原来一样。

loading

loading

3. 因为我们之前在BloomAddPassFragment方法和BloomScatterFinalPassFragment方法中返回的Alpha值为1,Bloom效果为不透明,我们进行调整,使得它能保持高分辨率源纹理的原始透明度。

float4 BloomAddPassFragment(Varyings input) : SV_TARGET 
{
...
float4 highRes = GetSource2(input.screenUV);
return float4(lowRes * _BloomIntensity + highRes.rgb, highRes.a);
}

float4 BloomScatterFinalPassFragment(Varyings input) : SV_TARGET
{
...
float4 highRes = GetSource2(input.screenUV);
lowRes += highRes.rgb - ApplyBloomThreshold(highRes.rgb);
return float4(lerp(highRes.rgb, lowRes, _BloomIntensity), highRes.a);
}

loading

13.1.3 Alpha分层

我们目前的分层方法只有在着色器提供合理的Alpha值才能和相机层混合,我们之前不关心写入的Alpha值是因为从来没使用它们做过任何事情。

现在如果两个Alpha为0.5的对象最终渲染到相同的纹素,那么该纹素最终的Alpha应该为0.25。当这两个对象中任何一个的Alpha为1时,最终结果都应为1;当第二个对象Alpha为0时应该保持原始的Alpha值。混合Alpha时,使用One OneMinusSrcAlpha可以覆盖所有这些情况。通过在混合模式之后添加逗号,然后添加透明度模式,来使用单独的因子混合透明通道。

1. 我们为Lit.shader和Unlit.shader进行该操作。

 //定义混合模式
Blend[_SrcBlend][_DstBlend], One OneMinusSrcAlpha

但这意味着进行深度写入的对象Alpha值始终为1,对于不透明对象来说不会出现什么问题。但如果它们使用的基础纹理包含不同的Alpha值就会出错,带有Alpha Clip的材质也可能出错,因为它们依靠Alpha阈值来舍弃片元。如果一个片元被裁切,则没什么问题,但如果不是,它的Alpha值应该为1。

2. 在LitInput和UnlitInput的UnityPerMaterial缓冲区中声明一个_ZWrite属性。

UNITY_DEFINE_INSTANCED_PROP(float, _ZWrite)

3. 定义一个新的GetFinalAlpha方法,如果_ZWrite不为1,则返回传入的Alpha值。

float GetFinalAlpha (float alpha) 
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _ZWrite) ? 1.0 : alpha;
}

4. 在LitPass.hlsl的LitPassFragment方法中调用该方法得到正确的Alpha值。

float4 LitPassFragment (Varyings input) : SV_TARGET 
{
...
return float4(color, GetFinalAlpha(surface.alpha));
}

5. UnlitPass中的UnlitPassFragment方法也做同样操作。

float4 UnlitPassFragment (Varyings input) : SV_TARGET 
{
...
return float4(base.rgb, GetFinalAlpha(base.a));
}

13.1.4 自定义混合

1. 与前一个相机层混合仅对叠加摄像机有意义,后一个相机将和相机目标的初始内容混合,这些内容是随机的或是前一帧的内容,除非编辑器设置了清除目标。所以第一个相机应使用One Zero模式进行混合,为了支持更换,叠加覆盖更多不同的分层选项,我们在启用后处理的相机中添加可以配置的最终混合模式,在Settings子目录下创建脚本CameraSettings.cs,进行相机的配置。我们定义一个FinalBlendMode结构体来存储源和目标的混合模式,默认使用One Zero。

using System;
using UnityEngine.Rendering;
[Serializable]
public class CameraSettings
{
[Serializable]
public struct FinalBlendMode
{
public BlendMode source, destination;
}

public FinalBlendMode finalBlendMode = new FinalBlendMode
{
source = BlendMode.One,
destination = BlendMode.Zero
};
}

2. 我们不能将这些设置直接添加到相机组件中,在Runtime子文件夹下我们创建一个CustomRenderPipelineCamera.cs脚本来作为扩展的相机组件,先定义一个CameraSettings字段和访问器,并保证该对象存在。

using UnityEngine;

[DisallowMultipleComponent, RequireComponent(typeof(Camera))]
public class CustomRenderPipelineCamera : MonoBehaviour
{
[SerializeField]
CameraSettings settings = default;
public CameraSettings Settings
{
get
{
if (settings == null)
{
settings = new CameraSettings();
}
return settings;
}
}
}

3. 我们在CameraRenderer.cs的Render方法中获取这个CustomRenderPipelineCamera组件,如果没有该组件存在,则使用一个默认设置对象。当调用postFXStack.Setup方法时,传递最终的混合模式。

static CameraSettings defaultCameraSettings = new CameraSettings();

public void Render(...)
{
this.context = context;
this.camera = camera;
var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
...
postFXStack.Setup(context, camera, postFXSettings, useHDR, colorLUTResolution,cameraSettings.finalBlendMode);
...
}

4. 在PostFXStack.cs中追踪相机的最终混合模式。

CameraSettings.FinalBlendMode finalBlendMode;
public void Setup(ScriptableRenderContext context, Camera camera, PostFXSettings settings, bool useHDR, int colorLUTResolution, CameraSettings.FinalBlendMode finalBlendMode)
{
...
this.finalBlendMode = finalBlendMode;
ApplySceneViewState();
}

5. 定义源和目标混合因子的着色器标识ID,并在DrawFinal方法中将混合因子传递到GPU,在设置渲染目标时,如果目标混合模式为0,则不需要关心目标缓冲区。

 int finalSrcBlendId = Shader.PropertyToID("_FinalSrcBlend");
int finalDstBlendId = Shader.PropertyToID("_FinalDstBlend");
void DrawFinal(RenderTargetIdentifier from)
{
buffer.SetGlobalFloat(finalSrcBlendId, (float)finalBlendMode.source);
buffer.SetGlobalFloat(finalDstBlendId, (float)finalBlendMode.destination);
buffer.SetGlobalTexture(fxSourceId, from);
buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
finalBlendMode.destination == BlendMode.Zero ? RenderBufferLoadAction.DontCare: RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
...
}

6. 最后我们将Final Pass的混合模式改为可配置的,没有设置混合模式的相机默认使用的是One Zero,叠加相机通常使用One、One Minus Src Alpha模式。

 Pass 
{
Name "Final"
Blend [_FinalSrcBlend] [_FinalDstBlend]
...
}

loading

13.1.5 GUI

还有一种常见的相机使用就是作为游戏界面GUI的一部分,在这种情况下相机的目标必须是RenderTexture才行。我们新建一个大小为200*100的渲染纹理资产,不使用深度缓冲,因为我们启用后处理进行相机渲染,它会创建自己的中间渲染纹理和深度缓冲区,然后将其赋予到相机组件的Target Texture属性中。

loading

​和常规渲染一样,底部的相机使用One Zero作为最终混合模式,这时编辑器Game视图应该是黑色的,所有渲染的内容存储在RenderTexure中。多个相机可以渲染到同一张渲染纹理中,区别是Unity在渲染到显示屏之前先渲染具有渲染纹理目标的相机(按深度递增的顺序渲染)。

现在,我们把渲染纹理赋予顶部的相机,再使用GameObject/UI/Raw Image创建一个Raw Image,并将渲染纹理赋予到Texture属性中。Raw Image使用默认的UI材质,该材质混合模式为SrcAlpha OneMinusSrcAlpha,因此透明度是正常的,但Bloom不能叠加,除非纹理显示像素完美的双线性滤波使相机的黑色背景色的黑色轮廓透明边缘可见。

loading

​要支持其它的混合模式,我们要创建一个用于UI的着色器,可以找到对应Untiy版本(我是Unity 2019.4.4)的Shader源码,通过DefaultResourcesExtra /UI找到Default-UI.shader,复制它并重命名为CustomBlending-UI.shader,属性栏添加_SrcBlend和_DstBlend使得混合模式可配置,然后调整SubShader中的Blend模式,其它代码基本不变,代码如下:

loading

​Shader "CustomRP/UI Custom Blending" 
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
}

SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Blend [_SrcBlend] [_DstBlend]
ColorMask [_ColorMask]
Cull Off
ZWrite Off
ZTest [unity_GUIZTestMode]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex UIPassVertex
#pragma fragment UIPassFragment
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct Attributes
{
float4 positionOS : POSITION;
float4 color : COLOR;
float2 baseUV : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 positionUI : VAR_POSITION;
float2 baseUV : VAR_BASE_UV;
float4 color : COLOR;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;

float4 _MainTex_ST;
float4 _Color;
float4 _TextureSampleAdd;
float4 _ClipRect;

Varyings UIPassVertex (Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);

UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = UnityObjectToClipPos(input.positionOS);
output.positionUI = input.positionOS.xy;
output.baseUV = TRANSFORM_TEX(input.baseUV, _MainTex);
output.color = input.color * _Color;
return output;
}
float4 UIPassFragment (Varyings input) : SV_Target
{
float4 color =
(tex2D(_MainTex, input.baseUV) + _TextureSampleAdd) * input.color;
#if defined(UNITY_UI_CLIP_RECT)
color.a *= UnityGet2DClipping(input.positionUI, _ClipRect);
#endif

#if defined(UNITY_UI_ALPHACLIP)
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}

13.1.6 逐相机的Post FX设置

1. 当场景中有多个相机时,我们应为它们添加可以使用不同后处理配置的支持,给CameraSettings脚本添加一个bool值用来控制是否覆盖全局的后处理配置,一个PostFXSettings字段设置该相机自己使用的后处理配置。

 public bool overridePostFX = false;
public PostFXSettings postFXSettings = default;

loading

2. 在CameraRenderer.cs的Render方法中判断是否需要覆盖全局后处理配置,如果需要,则将渲染管线的后处理配置替换成该相机的后处理配置。

 public void Render(...)
{
...
var crpCamera = camera.GetComponent<CustomRenderPipelineCamera>();
CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
//如果需要覆盖后处理配置,将渲染管线的后处理配置替换成该相机的后处理配置
if (cameraSettings.overridePostFX)
{
postFXSettings = cameraSettings.postFXSettings;
}
...
}

现在每个相机都可以使用默认的或自定义的后处理配置。我为渲染到纹理的相机提供一个用于测试的后处理配置,效果如下:

loading


13.2 渲染层

游戏场景中新建对象的默认层级都是在Default层,当我们想要使用不同的摄像机渲染不同的对象,比如UI相机渲染UI、人物相机渲染人物、场景相机渲染场景,那么就可以进行对应的层级设置,然后使用摄像机组件的Culling Mask属性有选择性地渲染不同层级的对象。

13.2.1 Culling Mask

不止相机,灯光也有Culling Mask属性,被灯光剔除的物体就像该光源不存在一样,该物体不会被光源照亮也不会投射阴影,但如果以定向光源进行尝试只会影响其阴影的投射,如下图所示:

loading

如果我们的渲染管线开启了useLightsPerObject逐对象光照选项,则非定向光源的剔除也可以生效。

loading

我们可以得到这样的结果,是因为灯光的Culling Mask被使用时,它将每个对象的光照索引发给了GPU,如果不使用这些剔除将无法正常工作,但它不适用于定向光源,因为我们将其应用于所有对象。阴影能被正确地剔除是因为从光的角度渲染阴影投射器时,光的Culling Mask就该跟摄像机的该属性达到一样的效果。

我们目前不能完全支持灯光的Culling Mask,HDRP也不支持。Unity提供渲染层作为SRP的替代方案,使用渲染层而不是游戏对象层有两个好处:第一是渲染器不局限于单个层级,这使得它们更加灵活;第二是渲染层不用于任何其它内容,不像Default层也用于物理。

在使用渲染层之前,当我们的光源的CullingMask不是设置Everything时,我们在光源的Inspector面板中弹出一个警告。在代码中,该枚举的-1代表Everything层。我们在CustomLightEditor.cs的OnInspectorGUI方法的最下面进行判断和警告提示。

 //重写灯光属性面板
public override void OnInspectorGUI()
{
...
//如果光源的CullingMask不是Everything层,显示警告:CullingMask只影响阴影
//如果不是定向光源,则提示除非开启逐对象光照,除了影响阴影还可以影响物体受光
var light = target as Light;
if (light.cullingMask != -1)
{
EditorGUILayout.HelpBox(light.type == LightType.Directional ?
"Culling Mask only affects shadows." :
"Culling Mask only affects shadow unless Use Lights Per Objects is on.",
MessageType.Warning);
}
}

loading

13.2.2 调整Rendering Layer Mask

1. 当使用SRP时,物体的Mesh Renderer组件会显示Rendering Layer Mask属性列表,该列表有32个层,默认显示的是Layer1、Layer2等等。可以重写RenderPipelineAsset.renderingLayerMaskNames的访问器为每个渲染管线配置这些层的名字,我们先将CustomRenderPipelineAsset类转换为局部类。

loading

public partial class CustomRenderPipelineAsset : RenderPipelineAsset

2. 创建一个用于编辑器的CustomRenderPipelineAsset.Editor脚本,在静态构造函数中为Rendering Layer Mask的层级重命名,我们在默认的命名中间添加一个空格作为新名字显示。

partial class CustomRenderPipelineAsset
{
#if UNITY_EDITOR
static string[] renderingLayerNames;
static CustomRenderPipelineAsset()
{
renderingLayerNames = new string[32];
for (int i = 0; i < renderingLayerNames.Length; i++)
{
renderingLayerNames[i] = "Layer " + (i + 1);
}
}
public override string[] renderingLayerMaskNames => renderingLayerNames;

#endif
}

然后我们可以调整Mesh Renderer组件的Rendering Layer Mask,但是光源的Rendering Layer Mask属性不能响应更改调整,我们不能直接解决这个问题,不过可以添加我们自己的有效版本属性。

3. 先在CustomLightEditor脚本中创建一个GUIContent对象,带有相同的标签以指示这是Rendering Layer Mask属性的可工作版本。

static GUIContent renderingLayerMaskLabel = new GUIContent("Rendering Layer Mask", "Functional version of above property.");

4. 然后创建一个DrawRenderingLayerMask方法,这是LightEditor.DrawRenderingLayerMask方法的替代方法,要让下拉菜单使用渲染管线的层名称,不能简单依靠EditorGUILayout.PropertyField。我们必须从设置中获取相关属性,确保处理多选项的混合值,抓取mask作为整数,显示它并将更改的值重新分配给属性,这是默认灯光检查器版本中缺少的最后一步。通过调用EditorGUILayout.MaskField来显示下拉列表。它需要标签、mask和GraphicsSettings.currentRenderPipeline.renderingLayerMaskNames作为参数。

 void DrawRenderingLayerMask() 
{
SerializedProperty property = settings.renderingLayerMask;
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int mask = property.intValue;
mask = EditorGUILayout.MaskField(renderingLayerMaskLabel, mask,
GraphicsSettings.currentRenderPipeline.renderingLayerMaskNames);
if (EditorGUI.EndChangeCheck())
{
property.intValue = mask;
}
EditorGUI.showMixedValue = false;
}

5. 在OnInspectorGUI方法中调用该方法,因此额外的Rendering Layer Mask属性直接显示在非功能性方法的下方,另外我们必须始终调用ApplyModifiedProperties方法来确保对Rendering Layer Mask属性的更改应用于灯光。

 public override void OnInspectorGUI() 
{
base.OnInspectorGUI();
DrawRenderingLayerMask();
if (!settings.lightType.hasMultipleDifferentValues &&(LightType)settings.lightType.enumValueIndex == LightType.Spot)
{
settings.DrawInnerAndOuterSpotAngle();
//settings.ApplyModifiedProperties();
}
settings.ApplyModifiedProperties();
...
}

loading

然后我们就可以调整下面的该属性来响应我们的操作。这时会发现,选择Everything和Layer 32层,都会变成Nothing层,这是因为光源的Rendering Layer Mask内部存储为无符号整数uint,这是必须的,因为它用于位掩码,但SerializedProperty仅支持获取和设置带符号的整数值。Everything由-1标识,该属性的Clamps为0,Layer32是最高位,代表比int.MaxValue大的数字,该属性也由0代替。

6. 我们可以将字符串数组由32的大小变为31,删除最后一层渲染层来解决第二个问题,我们还有很多层可以用,要知道HDRP也仅仅支持8层。

renderingLayerNames = new string[31];

7. 通过删除一层,Everything选项现在可以以一个值表示,该值除最高位外所有值都有设置,和int.MaxValue匹配。因此我们通过在存储int.MaxValue时显示-1来解决第一个问题,默认属性不这样做,这也是它为什么显示Mixed...而不是Everything的原因,HDRP也有这个问题。

 void DrawRenderingLayerMask() 
{
...
int mask = property.intValue;
if (mask == int.MaxValue)
{
mask = -1;
}
...
if (EditorGUI.EndChangeCheck())
{
property.intValue = mask == -1 ? int.MaxValue : mask;
}
EditorGUI.showMixedValue = false;
}

loading

8. 我们现在可以正确地调整灯光的Rendering Layer Mask属性了,但默认情况下不使用该Mask,因此没有更改任何内容。我们可以通过在实例化ShadowDrawingSettings对象时将useRenderingLayerMaskTest设置为true来将其应用到阴影,我们在RenderDirectionalShadows、RenderSpotShadows和RenderPointShadow方法中都进行此设置,应用于所有光源。现在我们可以通过配置物体和灯光的Rendering Layer Mask来消除阴影了。

 var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex)
{
useRenderingLayerMaskTest = true
};

13.2.3 向GPU发送Mask值

1. 下面将Rendering Layer Mask应用到我们Shader的光照计算里,对象和灯光的Mask都必须在GPU侧可用,首先要获取对象的掩码值,在UnityInput.hlsl的UnityPerDraw缓冲区中声明unity_RenderingLayer属性,掩码值存储在其X分量中。

real4 unity_WorldTransformParams;
float4 unity_RenderingLayer;

2. 将渲染层掩码添加到Surface结构体中,类型是uint,因为它是位掩码。

struct Surface 
{
...
uint renderingLayerMask;
};

3. 在LitPassFragment方法中设置表面的掩码时,使用asuint方法,这将使用原始数据,无需进行从float到uint类型的转换,因为这会更改位模式。

 surface.dither = InterleavedGradientNoise(input.positionCS.xy, 0);
surface.renderingLayerMask = asuint(unity_RenderingLayer.x);

4. 将掩码也添加到Light结构体中。

struct Light 
{
...
uint renderingLayerMask;
};

5. 我们后续要将掩码值发送到GPU,计划将该值存储在_DirectionalLightDirections和_OtherLightDirections的W分量中,为了后续方便阅读我们将Light.hlsl的_CustomLight缓冲区声明的这两个数组添加一个AndMasks后缀名作为标识。

CBUFFER_START(_CustomLight)
...
float4 _DirectionalLightDirectionsAndMasks[MAX_DIRECTIONAL_LIGHT_COUNT];
...
float4 _OtherLightDirectionsAndMasks[MAX_OTHER_LIGHT_COUNT];
...
CBUFFER_END

6. 在GetDirectionalLight和GetOtherLight方法中拿到掩码值。

Light GetDirectionalLight (int index,Surface surfaceWS, ShadowData shadowData) 
{
...
light.direction = _DirectionalLightDirectionsAndMasks[index].xyz;
light.renderingLayerMask = asuint(_DirectionalLightDirectionsAndMasks[index].w);
...
}
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
float3 spotDirection = _OtherLightDirectionsAndMasks[index].xyz;
light.renderingLayerMask = asuint(_OtherLightDirectionsAndMasks[index].w);
...
}

7. 调整Lighting.cs脚本的_DirectionalLightDirections和_OtherLightDirections相关的着色器标识ID和数组名字,使它们相匹配。然后调整SetupDirectionalLight、SetupPointLight和SetupSpotLight方法,添加一个Light传参,并获取灯光的renderingLayerMask值作为数组的W分量。

void SetupDirectionalLight(int index, int visibleIndex, ref VisibleLight visibleLight, Light light) 
{
dirLightColors[index] = visibleLight.finalColor;
//通过VisibleLight.localToWorldMatrix属性找到前向矢量,它在矩阵第三列,还要进行取反
Vector4 dirAndMask = -visibleLight.localToWorldMatrix.GetColumn(2);
dirAndMask.w = light.renderingLayerMask;
dirLightDirectionsAndMasks[index] = dirAndMask;
//存储阴影数据
dirLightShadowData[index] = shadows.ReserveDirectionalShadows(light,visibleIndex);
}

void SetupPointLight(int index, int visibleIndex, ref VisibleLight visibleLight, Light light)
{
...
Vector4 dirAndmask = Vector4.zero;
dirAndmask.w = light.renderingLayerMask;
otherLightDirectionsAndMasks[index] = dirAndmask;
//Light light = visibleLight.light;
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, visibleIndex);
}
void SetupSpotLight(int index, int visibleIndex, ref VisibleLight visibleLight, Light light)
{
...
Vector4 dirAndMask = -visibleLight.localToWorldMatrix.GetColumn(2);
dirAndMask.w = light.renderingLayerMask;
otherLightDirectionsAndMasks[index] = dirAndMask;
//Light light = visibleLight.light;
...
}

8. 在SetupLights方法每次循环遍历可见光源时,获取可见光的Light,并传递给上面三个方法。

 for (i = 0; i < visibleLights.Length; i++) 
{
int newIndex = -1;
VisibleLight visibleLight = visibleLights[i];
Light light = visibleLight.light;
switch (visibleLight.lightType)
{
case LightType.Directional:
if (dirLightCount < maxDirLightCount)
{
SetupDirectionalLight(dirLightCount++, i, ref visibleLight, light);
}
break;
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupPointLight(otherLightCount++, i, ref visibleLight, light);
}
break;
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupSpotLight(otherLightCount++, i, ref visibleLight, light);
}
break;
}
...
}

9. 在Lighting.hlsl中定义一个RenderingLayersOverlap方法,通过逻辑与的运算检查表面掩码和灯光掩码值是否重叠。

//检测表面掩码和灯光掩码是否重叠
bool RenderingLayersOverlap(Surface surface, Light light)
{
return (surface.renderingLayerMask & light.renderingLayerMask) != 0;
}

10. 下面我们在GetLighting的三个循环中检查是否要计算并叠加光照结果。

float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi) 
{
...
for (int i = 0; i < GetDirectionalLightCount(); i++)
{
Light light = GetDirectionalLight(i, surfaceWS, shadowData);
if (RenderingLayersOverlap(surfaceWS, light))
{
color += GetLighting(surfaceWS, brdf, light);
}
}

#if defined(_LIGHTS_PER_OBJECT)
for (int j = 0; j < min(unity_LightData.y, 8); j++)
{
int lightIndex = unity_LightIndices[(uint)j / 4][(uint)j % 4];
Light light = GetOtherLight(lightIndex, surfaceWS, shadowData);
if (RenderingLayersOverlap(surfaceWS, light))
{
color += GetLighting(surfaceWS, brdf, light);
}
}

#else
for (int j = 0; j < GetOtherLightCount(); j++)
{
Light light = GetOtherLight(j, surfaceWS, shadowData);
if (RenderingLayersOverlap(surfaceWS, light))
{
color += GetLighting(surfaceWS, brdf, light);
}
}

#endif
return color;
}

13.2.4 将int重新解释为float

1. 渲染层掩码此时会影响照明,但这样做不正确。灯光的RenderingLayerMask属性将位掩码设置为int类型,在三种光源的Setup方法转换成float类型时会出现乱码,我们无法向GPU发送一系列整数,因此必须以某种方式将int重置解释为float,而不是进行转换,C#无法像着色器那样调用asuint这样的方法解释数据,因为C#是强类型的。我们使用union结构来提供别名数据类型,通过向int添加ReinterpretAsFloat扩展方法来隐藏此方法,为此先创建一个静态的ReinterpretExtensions类,该类最初只是执行常规类型转换。

public static class ReinterpretExtensions
{
public static float ReinterpretAsFloat(this int value)
{
return value;
}
}

2. 在三种光源的Setup方法中使用ReinterpretAsFloat方法进行类型解释,而不是依赖隐性类型转换。

dirAndmask.w = light.renderingLayerMask.ReinterpretAsFloat();

3. 然后在ReinterpretExtensions中定义一个包含一个int和一个float类型的IntFloat结构体,在ReinterpretAsFloat方法初始化为此类型的默认值,设置其为整数值,然后返回float值。

 struct IntFloat 
{
public int intValue;
public float floatValue;
}

public static float ReinterpretAsFloat(this int value)
{
IntFloat converter = default;
converter.intValue = value;
return converter.floatValue;
}

4. 要将其转换为重新解释,需要使结构体的两个字段重叠,以便它们共享相同的数据。我们通过在结构体上方附加StructLayout属性,类型设置为LayoutKind.Explicit,使结构体布局明确,然后将FieldOffset属性添加到字段中,指明将字段的数据放在哪里,将两个偏移值设为0使它们重叠,这些属性来自System.Runtime.InteropServices命名空间。

using System.Runtime.InteropServices;

public static class ReinterpretExtensions
{
[StructLayout(LayoutKind.Explicit)]
struct IntFloat
{
[FieldOffset(0)]
public int intValue;
[FieldOffset(0)]
public float floatValue;
}
...
}

现在该结构体int和float字段是相同的数据,但解释不一样。这样能使位掩码完整,并且Rendering Layer Mask可以正常工作。我们把右边的Cube的MeshRenderer组件的Rendering Layer Mask设置Layer2,然后平行光的Rendering Layer Mask不勾选Layer2,其它Layer全部勾选,即右边的Cube不受光照影响,也不产生阴影。

loading

13.2.5 相机的Rendering Layer Mask

1. 相机除了使用Culling Mask,还能使用Rendering Layer Mask来限制相机的渲染,Camera组件本身没有该属性,但我们可以在CameraSettings中进行扩展。将它设置为int类型,默认值为−1,表示所有层。

public int renderingLayerMask = -1;

loading

2. 下面我们需要为Rendering Layer Mask属性做一个下拉菜单GUI,创建一个继承PropertyAttribute的RenderingLayerMaskFieldAttribute类进行处理。这只是一个不需要执行任何操作的标记属性,这不是编辑器类型,因此不能放在Editor文件夹中。

using UnityEngine;

public class RenderingLayerMaskFieldAttribute : PropertyAttribute
{

}

3. 将该属性添加到Rendering Layer Mask字段上面。

 [RenderingLayerMaskField]
public int renderingLayerMask = -1;

4. 创建一个继承PropertyDrawer的RenderingLayerMaskDrawer类,将CustomLightEditor脚本的DrawRenderingLayerMask方法复制过来并重命名为Draw。然后给它三个参数:位置Rect、序列化属性和GUIContent标签。使用EditorGUI.MaskField方法替换EditorGUILayout.MaskField方法。

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
[CustomPropertyDrawer(typeof(RenderingLayerMaskFieldAttribute))]
public class RenderingLayerMaskDrawer : PropertyDrawer
{
public static void Draw(Rect position, SerializedProperty property, GUIContent label)
{
//SerializedProperty property = settings.renderingLayerMask;
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
int mask = property.intValue;
bool isUint = property.type == "uint";
if (isUint && mask == int.MaxValue)
{
mask = -1;
}
mask = EditorGUI.MaskField(position, label, mask,GraphicsSettings.currentRenderPipeline.renderingLayerMaskNames);
if (EditorGUI.EndChangeCheck())
{
property.intValue = isUint && mask == -1 ? int.MaxValue : mask;
}
EditorGUI.showMixedValue = false;
}
}

5. 然后重写OnGUI方法,只需将其调用转发到Draw方法。

 public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
Draw(position, property, label);
}

6. 为了和CustomLightEditor脚本结合使用,新建一个没有Rect参数的Draw方法,通过调用EditorGUILayout.GetControlRect方法获取单行位置Rect。

 public static void Draw(SerializedProperty property, GUIContent label)
{
Draw(EditorGUILayout.GetControlRect(), property, label);
}

7. 让CustomLightEditor.OnInspectorGUI()调用RenderingLayerMaskDrawer.Draw方法,DrawRenderingLayerMask方法可以删掉了。

 public override void OnInspectorGUI()
{
base.OnInspectorGUI();
//DrawRenderingLayerMask();
RenderingLayerMaskDrawer.Draw(settings.renderingLayerMask, renderingLayerMaskLabel);
...
}

8. 最后要在相机渲染器中应用Rendering Layer Mask,给DrawVisibleGeometry方法添加一个renderingLayerMask参数,在实例化FilteringSettings对象时传递给它,并转换为unit类型。然后在Render方法调用DrawVisibleGeometry时将参数传递过去。

void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject, int renderingLayerMask)
{
...
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque, renderingLayerMask: (uint)renderingLayerMask);
...
}

public void Render(...)
{
...
//绘制几何体
DrawVisibleGeometry(useDynamicBatching, useGPUInstancing,useLightsPerObject, cameraSettings.renderingLayerMask);
...
}

现在我们可以使用更灵活的Rendering Layer Mask来控制摄像机渲染的内容。例如,可以绘制一些物体的阴影,即使相机不绘制这些物体。

13.2.6 逐相机的灯光Mask设置

1. 我们还可以给每个相机设置不同的灯光Mask。我们将再次为此使用渲染层,但由于它是非标准和必须行为,因此在 CameraSettings脚本中添加一个切换开关来使其成为可选项。

public bool maskLights = false;

loading

2. 给Lighting脚本的SetupLights方法添加一个renderingLayerMask传参,然后检测每个光源的renderingLayerMask是否和传入的渲染层掩码值重叠,如果不重叠则继续执行switch语句。

void SetupLights(bool useLightsPerObject, int renderingLayerMask) 
{
...
for (i = 0; i < visibleLights.Length; i++)
{
...
if ((light.renderingLayerMask & renderingLayerMask) != 0)
{
switch (visibleLight.lightType)
{
...
}
}
//匹配光源索引
if (useLightsPerObject)
{
indexMap[i] = newIndex;
}
}
...
}

3. 在Setup方法调用SetupLights方法时传入当前相机的Rendering Layer Mask值。

public void Setup(ScriptableRenderContext context, CullingResults cullingResults,ShadowSettings shadowSettings, bool useLightsPerObject, int renderingLayerMask)
{
...
//发送光源数据
SetupLights(useLightsPerObject, renderingLayerMask);
...
}

4. 在CameraRenderer脚本的Render方法调用lighting.Setup方法时判断是否使用该相机的渲染层掩码值应用于灯光,否则使用-1。

lighting.Setup(context, cullingResults, shadowSettings, useLightsPerObject, cameraSettings.maskLights ? cameraSettings.renderingLayerMask : -1);

最后我们使用两个相机渲染相同的场景,其中左边相机的物体只受定向光影响,右边相机的物体只受点光源影响(注意:这种操作只适用于实时照明,烘焙照明不受影响)。我的场景的设置为:所有物体的Rendering Layer Mask设置为Everything,定向光设置为Layer1单层,点光源设置为Layer2单层,左边相机的Rendering Layer Mask设置为除Layer2以外的所有层,右边相机设置除Layer1以外所有层,最后结果如下图所示。

loading

源代码及PDF课件地址:

文件下载
暂无评论发表评论